/*
 * Project: Sudoku Hint Engine for Android
 * Copyright (C) 2010-2013 Emanuel Moecklin
 * 
 * Available under the terms of the Lesser General Public License (LGPL)
 */
package com.onegravity.sudoku.solver;

import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.TreeSet;

import com.onegravity.sudoku.solver.accumulator.HintsAccumulator;
import com.onegravity.sudoku.solver.accumulator.StopHintProductionException;
import com.onegravity.sudoku.solver.core.Hint;
import com.onegravity.sudoku.solver.util.Logger;

/**
 * The HintCollector runs in the background to collect hints.
 * It filters and sorts them and has methods to retrieve them.
 * 
 * IMPORTRANT: the collector can be started but once!
 * If you need to collect more hints, create another HintCollector.
 * 
 * IMPORTANT: note that some hint producers modify the grid temporarily.
 * The according pieces of code are always synchronized on the grid.
 * Therefore other threads must stop the hint collection and wait for the lock on the grid to be
 * released before they read/write access it (by using a synchronized statement on the grid object).
 * 
 * @author Emanuel Moecklin
 */
public class HintCollector extends Thread {
	
	private static final String LOGGER_TAG = HintCollector.class.getSimpleName();

	// Constructor parameters
	private HintEngine hintEngine;
	private boolean doAnalysis;
	private HintCollectorListener listener;
	private int maxNrOfHints;

	private boolean isCollecting;
	private HintFilter filter;
	private TreeSet<Hint> hints;
	
	private Thread collectorThread;

    // ****************************************** HintCollector *******************************************

	/**
	 * @param hintEngine The hint engine to produce the hints.
	 * @param listener HintCollectorListener that is informed once a hint has been collected
	 * @param doAnalysis If True runs an anlaysis of the Sudoku, if False collects hints
	 * @param maxNrOfHints If 0 then all the hints the HintEngine finds are collected,
	 * if >0 then the number of collected hints is limited
	 */
	public HintCollector(HintEngine hintEngine, HintCollectorListener listener, boolean doAnalysis, int maxNrOfHints) {
		this.hintEngine = hintEngine;
		this.doAnalysis = doAnalysis;
		this.listener = listener;
		this.maxNrOfHints = maxNrOfHints;

		isCollecting = false;
		filter = new HintFilter();
		hints = new TreeSet<Hint>(new HintComparator());
	}
	
	public void run() {
		
		synchronized(this) {
			// collector can be started just once!
			if (isCollecting) return;
			isCollecting = true;
			filter.reset();
		}

		collectorThread = Thread.currentThread();
        if (doAnalysis) {
        	Hint hint = hintEngine.analyze();
        	try {
				acc.add(hint);
			} catch (StopHintProductionException willNotHappen) {}
        }
        else {
        	hintEngine.getAllHints(acc, listener);
        }
		isCollecting = false;
	}
	
	public void stopCollecting() {
		synchronized(this) {
			if (isCollecting && collectorThread!=null) collectorThread.interrupt();
		}
	}

	public boolean isCollecting() {
		return isCollecting;
	}
	
	private HintsAccumulator acc = new HintsAccumulator() {

    	private long hintCounter = 0;
    	
    	@Override
    	public void add(Hint hint) throws StopHintProductionException {
    		if (hint!=null) {
	    		synchronized(hints) {
	    			if (filter.isEligible(hint)) {
	    				hint.setSortKey(hintCounter++);
	    				hints.add(hint);
	    				if (listener!=null) listener.onHintAdded(hint);
	    				if (hintCounter==maxNrOfHints) throw new StopHintProductionException();
	    			}
	    			else {
	    				Logger.d(LOGGER_TAG, "hint is not eligible: " + hint.toString()); //$NON-NLS-1$
	    			}
	    		}
    		}
    	}

		@Override
		public List<Hint> getHints() {
			ArrayList<Hint> list = new ArrayList<Hint>();
			list.addAll(hints);
			return list;
		}
		
    };
    
	// ********** getNext() **********
	
	/**
	 * @return True if the hint has a next hint, False otherwise.
	 */
	public boolean hasNext(Hint hint) {
		synchronized(hints) {
			return (hint==null || !hints.contains(hint)) ? (hints.size()>0) : (hints.tailSet(hint).size()>1);
		}
	}
	
	/**
	 * @param hint We want the next hint to that one, may also be null --> the first hint will be returned
	 * @return The next hint if there is one or Null otherwise.
	 */
	public Hint getNext(Hint hint) {
		synchronized(hints) {
			Hint nextHint = null;
			try {
				if (hint==null || !hints.contains(hint)) {
					nextHint = hints.first();
				}
				else {
					// find second hint in hints.tailSet(hint) because tailSet returns tail including tail
					HintComparator comp = new HintComparator();
					for (Hint theHint:hints.tailSet(hint)) {
						if (comp.compare(theHint, hint)!=0) {
							nextHint = theHint;
							break;
						}
					}
				}
			}
			catch (NoSuchElementException e) {}
			return nextHint;
		}
	}

	// ********** previous() **********
	
	/**
	 * @return True if the hint has a previous hint, False otherwise.
	 */
	public boolean hasPrevious(Hint hint) {
		synchronized(hints) {
			return (hint==null) ? (hints.size()>0) : (hints.headSet(hint).size()>0);
		}
	}

	/**
	 * @param hint We want the previous hint to that one, may also be null --> the first hint will be returned
	 * @return The previous hint if there is one or Null otherwise.
	 */
	public Hint getPrevious(Hint hint) {
		synchronized(hints) {
			Hint previousHint = null;
			try {
				previousHint = (hint==null) ? hints.last() : hints.headSet(hint).last();
			}
			catch (NoSuchElementException e) {}
			return previousHint;
		}
	}

}
